<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
    />
    <title>Document</title>
    <style>
      * {
        padding: 0;
        margin: 0;
        box-sizing: border-box;
      }
      body {
        width: 100vw;
        height: 100vh;
        overflow: hidden;
      }
      .box {
        /* margin: 100px auto; */
        padding: 12px;
        display: flex;
        flex-wrap: wrap;
        gap: 10px;
        width: 100%;
        border: 1px solid #ccc;
        box-shadow: 0px 0px 3px #ccc;
        border-radius: 4px;
        box-sizing: border-box;
        overflow: auto;
        user-select: none;
      }

      .box-item {
        --delay: 0s;
        border-radius: 16px;
        cursor: pointer;
        padding: 7px 13px;
        transition: 0.3s;
      }

      /* .box-item:where(.current, :hover) { */
      .box-item.current {
        background-color: #ebf5fd;
        color: #0a82e5;
      }

      /* .box-item.start {
        background-color: #ebf5fd;
        color: #0a82e5;
      } */

      /* .box-item.end {
        background-color: #ebf5fd;
        color: #0a82e5;
      } */

      @media screen and (min-width: 800px) {
        .box {
          width: 400px;
          margin: 100px auto;
        }
      }
    </style>
  </head>
  <body>
    <div class="box">
      <div class="box-item" data-index="1">
        <span>包1</span>
      </div>
      <div class="box-item" data-index="2">
        <span>包2</span>
      </div>
      <div class="box-item" data-index="3">
        <span>包3</span>
      </div>
      <div class="box-item" data-index="4">
        <span>包4</span>
      </div>
      <div class="box-item" data-index="5">
        <span>包5</span>
      </div>
      <div class="box-item" data-index="6">
        <span>包6</span>
      </div>
      <div class="box-item" data-index="7">
        <span>包7</span>
      </div>
      <div class="box-item" data-index="8">
        <span>包8</span>
      </div>
      <div class="box-item" data-index="9">
        <span>包9</span>
      </div>
      <div class="box-item" data-index="10">
        <span>包10</span>
      </div>
      <div class="box-item" data-index="11">
        <span>包11</span>
      </div>
      <div class="box-item" data-index="12">
        <span>包12</span>
      </div>
      <div class="box-item" data-index="13">
        <span>包13</span>
      </div>
      <div class="box-item" data-index="14">
        <span>包14</span>
      </div>
      <div class="box-item" data-index="15">
        <span>包15</span>
      </div>
      <div class="box-item" data-index="16">
        <span>包16</span>
      </div>
      <div class="box-item" data-index="17">
        <span>包17</span>
      </div>
      <div class="box-item" data-index="18">
        <span>包18</span>
      </div>
      <div class="box-item" data-index="19">
        <span>包19</span>
      </div>
      <div class="box-item" data-index="20">
        <span>包120</span>
      </div>
    </div>
    <script>
      const isWindow = /window/i.test(navigator.userAgent);
      const boxWrap = document.querySelector(".box");
      const boxWrapPosition = {
        left: boxWrap.offsetLeft,
        top: boxWrap.offsetTop,
        width: boxWrap.offsetWidth,
        height: boxWrap.offsetHeight,
      };
      const boxList = boxWrap.querySelectorAll(".box-item");
      const boxItemPositionMap = new WeakMap();
      const rowHeightConfig = {};
      const keyDownConfig = {
        downTarget: null,
        moveTarget: null,
      };
      let lastSelectList = [];
      let currentSelectList = [];

      function boxWrapMouseUp(e) {
        const { clientX, clientY, target } = e?.changedTouches?.[0] || e;
        const realX = clientX - boxWrapPosition.left,
          realY = clientY - boxWrapPosition.top;
        let boxItemTarget = isWindow ? target.closest(".box-item") : null;
        if (!boxItemTarget) {
          boxItemTarget = locateBoxItem({
            left: realX,
            top: realY,
          });
        }

        // 如果点击和放手的元素相同，则说明要么是选中当前元素，要么是取消当前选择的选中
        if (keyDownConfig.downTarget === boxItemTarget) {
          if (boxItemTarget.classList.contains("current")) {
            boxItemTarget.classList.remove("current");
          } else {
            boxItemTarget.classList.add("current");
          }
        }

        // 记录上次选中的
        lastSelectList = [];
        currentSelectList = [];
        const selectDomList = boxWrap.querySelectorAll(".current");
        selectDomList.forEach((dom) => {
          lastSelectList.push(dom.dataset.index * 1);
        });

        keyDownConfig.downTarget = null;
        keyDownConfig.moveTarget = null;
        if (isWindow) {
          window.removeEventListener("mousemove", boxWrapMousemove);
          window.removeEventListener("mouseup", boxWrapMouseUp);
        } else {
          window.removeEventListener("touchmove", boxWrapMousemove);
          window.removeEventListener("touchend", boxWrapMouseUp);
        }
      }

      function boxWrapMousemove(e) {
        e.preventDefault();
        e.stopPropagation();

        const { clientX, clientY, target } = e?.touches?.[0] || e;
        const realX = clientX - boxWrapPosition.left,
          realY = clientY - boxWrapPosition.top;
        let boxItemTarget = isWindow ? target.closest(".box-item") : null;
        if (!boxItemTarget) {
          boxItemTarget = locateBoxItem({
            left: realX,
            top: realY,
          });
        }

        if (keyDownConfig.downTarget === boxItemTarget) {
          if (
            keyDownConfig.moveTarget &&
            keyDownConfig.moveTarget !== boxItemTarget
          ) {
            resetCurrent(
              keyDownConfig.downTarget.dataset.index,
              keyDownConfig.downTarget.dataset.index
            );
            console.log('xxx')
          }
          return;
        }

        if (
          keyDownConfig.moveTarget?.dataset.index ===
          boxItemTarget.dataset.index
        ) {
          return;
        }

        keyDownConfig.moveTarget = boxItemTarget;

        const startIndex = keyDownConfig.downTarget.dataset.index;
        const endIndex = keyDownConfig.moveTarget.dataset.index;

        resetCurrent(startIndex, endIndex);
      }

      function boxWrapMouseDown(e) {
        const { clientX, clientY, target } = e?.touches?.[0] || e;
        const realX = clientX - boxWrapPosition.left,
          realY = clientY - boxWrapPosition.top;

        let boxItemTarget = isWindow ? target.closest(".box-item") : null;
        if (!boxItemTarget) {
          boxItemTarget = locateBoxItem({
            left: realX,
            top: realY,
          });
        }

        keyDownConfig.downTarget = boxItemTarget;
        if (isWindow) {
          window.addEventListener("mousemove", boxWrapMousemove);
          window.addEventListener("mouseup", boxWrapMouseUp);
        } else {
          window.addEventListener("touchmove", boxWrapMousemove, {
            passive: false,
          });
          window.addEventListener("touchend", boxWrapMouseUp, {
            passive: false,
          });
        }
      }

      function bindEvent() {
        if (isWindow) {
          boxWrap.addEventListener("mousedown", boxWrapMouseDown);
        } else {
          boxWrap.addEventListener("touchstart", boxWrapMouseDown, {
            passive: false,
          });
        }
      }

      // 重置当前激活项
      function resetCurrent(start, end) {
        const _localSelect = [];
        const min = Math.min(start, end);
        const max = Math.max(start, end);
    
        boxList.forEach((item, index) => {
          const realIndex = index + 1;
          if (realIndex >= min && realIndex <= max) {
            // 对应上次，上次选择的这次还选择得取消
            if (lastSelectList.includes(realIndex)) {
              item.classList.remove("current");
            } else {
              item.classList.add("current");
              _localSelect.push(realIndex);
            }
          } else {
            if (currentSelectList.includes(realIndex)) {
              item.classList.remove('current')
            }
          }
        });

        currentSelectList = _localSelect;
      }

      // 依据位置判断当前点击的是那个元素
      function locateBoxItem({ left, top }) {
        const heightList = Reflect.ownKeys(rowHeightConfig);
        const heightIndex = binarySearch(heightList, top);

        const rowConfigList = rowHeightConfig[heightList[heightIndex]];
        const leftList = rowConfigList.map(
          (item) => item.left + item.width / 2
        );
        const leftIndex = binarySearch(leftList, left, true);

        return rowConfigList[leftIndex].target;
      }

      // 计算每个boxItem的位置
      function calcListItemPositionInfo() {
        for (const itemNode of boxList) {
          const itemTop = itemNode.offsetTop - boxWrapPosition.top;
          const itemLeft = itemNode.offsetLeft - boxWrapPosition.left;

          if (rowHeightConfig[itemTop]) {
            rowHeightConfig[itemTop].push({
              target: itemNode,
              left: itemLeft,
              width: itemNode.offsetWidth,
              height: itemNode.offsetTop,
            });
          } else {
            rowHeightConfig[itemTop] = [
              {
                target: itemNode,
                left: itemLeft,
                width: itemNode.offsetWidth,
                height: itemNode.offsetTop,
              },
            ];
          }

          boxItemPositionMap.set(itemNode, {
            left: itemNode.offsetLeft - boxWrapPosition.left,
            top: itemTop,
            width: itemNode.offsetWidth,
            height: itemNode.offsetTop,
          });
        }
      }

      // 二分查找
      function binarySearch(data, target, needDetermineDistance = false) {
        let index = 0;
        const len = data.length;
        let start = 0,
          last = len - 1;

        if (Number.isNaN(target)) {
          return start;
        }

        if (target >= data[last]) {
          return last;
        }

        if (target <= data[start]) {
          return start;
        }

        while (start <= last) {
          const mid = Math.floor((start + last) / 2);
          const midValue = data[mid];

          if (index > len) {
            console.log("溢出: ", data, target);
            return;
          }

          if (midValue == target) {
            return mid;
          }

          if (target > midValue && target < data[mid + 1]) {
            if (needDetermineDistance) {
              const curRemainder = Math.abs(target - midValue);
              const nextRemainder = Math.abs(target - data[mid + 1]);
              return curRemainder <= nextRemainder ? mid : mid + 1;
            }
            return mid;
          }

          if (midValue > target) {
            last = mid - 1;
          } else if (midValue < target) {
            start = mid + 1;
          }

          index++;
        }
      }

      function init() {
        calcListItemPositionInfo();
        bindEvent();

        console.log(rowHeightConfig)
      }

      init();
    </script>
  </body>
</html>

